package top.syfy.jwt.jwtdemo.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import top.syfy.jwt.jwtdemo.bean.UserBean;
import top.syfy.jwt.jwtdemo.exception.CustomerException;
import top.syfy.jwt.jwtdemo.exception.TokenIllegalException;
import top.syfy.jwt.jwtdemo.exception.TokenMisMatchException;
import top.syfy.jwt.jwtdemo.message.CodeEnum;
import top.syfy.jwt.jwtdemo.redis.RedisProvide;
import top.syfy.jwt.jwtdemo.util.Object2MapUtil;
import java.lang.reflect.InvocationTargetException;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * JWT服务类, 用于产生、校验、刷新JWT令牌
 * 使用@ConfigurationProperties完成application.yml中定义的配置信息，自动绑定到类中的属性
 * 1.@ConfigurationProperties:为属性配置错误的值时，而又不希望SpringBoot应用启动失败，可以设置ignoreInvalidFields属性为true(默认为 false)
 * 2.默认情况下，SpringBoot会忽略那些不能绑定到@ConfigurationProperties类中的属性. 然而当配置文件中的某些属性实际上没有绑定到@ConfigurationProperties类中的属性时，
 * 我们可能希望启动失败, 因此仅需要将ignoreUnknownFields属性设置为false(默认是true)
 *
 * @author syfy
 * @date 2021/03/05
 */
@SuppressWarnings({"all"})
@Data
@Component
@ConfigurationProperties(prefix = "jwt", ignoreInvalidFields = true, ignoreUnknownFields = false)
public class JwtProvide {

	@Autowired
	private RedisProvide redisProvide;

	/**
	 * 私钥
	 */
	private String secret = null;

	/**
	 * 发布者
	 */
	private String issuser = "www.syfy.top";

	/**
	 * 主题
	 */
	private String subject = "userLoginToken";

	/**
	 * 受众
	 */
	private List<String> audiences = new ArrayList<>();

	/**
	 * 令牌自颁发时刻起的过期时长(毫秒),默认为1小时
	 */
	@DurationUnit(ChronoUnit.MILLIS)
	private Long expired = 3600000L;

	/**
	 * 令牌刷新时间(毫秒),默认为30分钟
	 */
	@DurationUnit(ChronoUnit.MILLIS)
	private Long refresh = 1800000L;

	/**
	 * 自定义签名(其他自定义数据)
	 */
	private Map<String, Object> claims = new HashMap<>();

	/**
	 * 创建默认时长有效期(毫秒)、默认刷新时间(毫秒)的令牌
	 *
	 * @param claims 自定义签名(其他自定义数据)
	 * @return 具有默认时长有效期的令牌
	 */
	public String createToken(Map<String, Object> claims) {
		return createToken(claims, expired, refresh);
	}

	/**
	 * 创建指定有效期、指定刷新时间的令牌
	 *
	 * @param claims  自定义签名(其他自定义数据)
	 * @param expired 指定有效期(毫秒)
	 * @param refresh 指定刷新时间(毫秒)
	 * @return 具有指定有效期、指定刷新时间的令牌
	 */
	public String createToken(Map<String, Object> claims, Long expired, Long refresh) {
		Payload payload = this.createPayload(expired, refresh);
		payload.setClaims(claims);
		Algorithm algorithm = Algorithm.HMAC256(this.getSecret());
		return createToken(payload, algorithm);
	}

	/**
	 * 根据负载和算法(HMAC256)创建令牌
	 *
	 * @param payload   负载数据
	 * @param algorithm 算法(HMAC256)
	 * @return 根据负载和算法创建令牌
	 */
	private String createToken(Payload payload, Algorithm algorithm) {
		/* JWT头部 */
		JWTCreator.Builder headerBuilder = createHeaderBuilder(algorithm);
		/* JWT负载 */
		// 负载的公共声明部分
		JWTCreator.Builder claimBuilder_public = addClaimBuilderPublic(headerBuilder, payload);
		// 负载的私有声明部分
		JWTCreator.Builder claimBuilder_private = addClaimBuilderPrivate(claimBuilder_public, payload);
		/* JWT签名 */
		String token = claimBuilder_private.sign(algorithm);
		return token;
	}

	/**
	 * 添加公共声明
	 *
	 * @param builder JWT头部
	 * @param payload 载荷
	 * @return 公共声明
	 */
	private JWTCreator.Builder addClaimBuilderPublic(JWTCreator.Builder builder, Payload payload) {
		// 生成发布者(若没有指定值,则使用默认值)
		if (StringUtils.hasLength(payload.getIssuer())) {
			builder.withIssuer(payload.getIssuer());
		}
		// 生成主题(若没有指定值,则使用默认值)
		if (StringUtils.hasLength(payload.getSubject())) {
			builder.withSubject(payload.getSubject());
		}
		// 生成签名时间
		if (Objects.nonNull(payload.getIssuedAt())) {
			builder.withIssuedAt(new Date(payload.getIssuedAt()));
		}
		// 生成过期时间
		if (Objects.nonNull(payload.getExpiresAt())) {
			builder.withExpiresAt(new Date(payload.getExpiresAt()));
		}
		// 开始使用时间
		if (Objects.nonNull(payload.getNotBefore())) {
			builder.withNotBefore(new Date(payload.getNotBefore()));
		}
		// 生成受众(若没有指定值,则使用默认值)
		if (!CollectionUtils.isEmpty(payload.getAudiences())) {
			payload.getAudiences().forEach(builder::withAudience);
		}
		return builder;
	}

	/**
	 * 添加私有声明(自定义数据部分)
	 *
	 * @param builder 公共声明
	 * @param payload 载荷
	 * @return 私有声明
	 */
	private JWTCreator.Builder addClaimBuilderPrivate(JWTCreator.Builder builder, Payload payload) {
		Map<String, Object> claims = payload.getClaims();
		if (!CollectionUtils.isEmpty(claims)) {
			claims.forEach((k, v) -> builder.withClaim(k, String.valueOf(v)));
		}
		builder.withClaim("refreshAt", payload.getRefreshAt());
		return builder;
	}

	/**
	 * 创建指定过期时间和指定刷新时间的负载数据
	 *
	 * @param expired 指定过期时间
	 * @param refresh 指定刷新时间
	 * @return 指定过期时间和指定刷新时间的负载数据
	 */
	private Payload createPayload(Long expired, Long refresh) {
		Long now = new Date().getTime();
		Payload payload = new Payload();
		payload.setIssuer(this.getIssuser());
		payload.setSubject(this.getSubject());
		payload.setAudiences(this.getAudiences());
		payload.setIssuedAt(now);
		payload.setNotBefore(now);
		payload.setExpiresAt(now + this.getExpired());
		payload.setRefreshAt(now + this.getRefresh());
		return payload;
	}

	/**
	 * 创建JWT头部信息
	 *
	 * @param algorithm 算法
	 * @return JWT头部信息
	 */
	private JWTCreator.Builder createHeaderBuilder(Algorithm algorithm) {
		return JWT.create().withHeader(builderJWTHeader(algorithm));
	}

	/**
	 * 构建JWT头部信息(alg、typ)的Map信息
	 *
	 * @param algorithm 算法
	 * @return JWT头部信息(alg 、 typ)的Map信息
	 */
	private Map<String, Object> builderJWTHeader(Algorithm algorithm) {
		Map<String, Object> map = new HashMap<>();
		map.put("alg", algorithm.getName());
		map.put("typ", "JWT");
		return map;
	}

	/**
	 * 校验令牌
	 *
	 * @param token 待校验的令牌
	 */
	public Payload verifyToken(String token) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
		DecodedJWT jwt = getDecodedJWT(token);
		Payload payload = getPrivateClaim(jwt, getPublicClaim(jwt));
		Map<String, Object> claims = payload.getClaims();
		UserBean userBean = Object2MapUtil.map2Bean(claims, UserBean.class);
		// 判断解析出来的对象是否与Redis中存储的对象在属性上是否一致;若是不一致,需要抛出异常
		UserBean userBeanRedis = (UserBean) redisProvide.get(token);
		if (Objects.isNull(userBeanRedis)) {
			throw new TokenIllegalException(token);
		}
		if (!Objects.equals(userBeanRedis.getId(), userBean.getId()) || !Objects.equals(userBeanRedis.getUserName(), userBean.getUserName())) {
			throw new TokenMisMatchException(token);
		}
		return payload;
	}

	/**
	 * 获取JWT负载中的公有声明
	 *
	 * @param jwt 从其它字符串表示形式解码的JWT的类实例
	 * @return 包含JWT令牌中的公有声明的JWT负载的封装类实例
	 */
	private Payload getPublicClaim(DecodedJWT jwt) {
		Payload payload = new Payload();
		payload.setIssuer(jwt.getIssuer());
		payload.setSubject(jwt.getSubject());
		payload.setAudiences(jwt.getAudience());
		payload.setIssuedAt(jwt.getIssuedAt().getTime());
		payload.setExpiresAt(jwt.getExpiresAt().getTime());
		return payload;
	}

	/**
	 * 获取JWT负载中的私有声明
	 *
	 * @param jwt     从其它字符串表示形式解码的JWT的类实例
	 * @param payload JWT负载的封装类实例
	 * @return 包含JWT令牌中的公有声明和私有声明的JWT负载的封装类实例
	 */
	private Payload getPrivateClaim(DecodedJWT jwt, Payload payload) {
		Map<String, Object> claims = new HashMap<>();
		jwt.getClaims().forEach(claims::put);
		payload.setClaims(claims);
		return payload;
	}

	/**
	 * 获取从其它字符串表示形式解码的JWT的类实例
	 *
	 * @param token 令牌
	 * @return 从其它字符串表示形式解码的JWT的类实例
	 */
	private DecodedJWT getDecodedJWT(String token) {
		JWTVerifier verifier = JWT.require(Algorithm.HMAC256(this.getSecret())).build();
		DecodedJWT jwt = verifier.verify(token);
		return jwt;
	}

	/**
	 * 刷新新新的令牌
	 *
	 * @param token 旧的令牌
	 * @return 新的令牌
	 */
	public String refreshToken(String token) {
		DecodedJWT jwt = getDecodedJWT(token);
		Payload payload = getPrivateClaim(jwt, getPublicClaim(jwt));
		Map<String, Object> claims = payload.getClaims();
		// 刷新时间
		Claim claim = (Claim) claims.get("refreshAt");
		Date refreshAt = claim.asDate();
		// 过期时间
		Date expiresAt = new Date(payload.getExpiresAt());
		Date currentAt = new Date();
		// 当前时间未超过过期时间且超过了刷新时间, 那么就刷新令牌
		if (currentAt.before(expiresAt) && refreshAt.before(currentAt)) {
			UserBean userBean = (UserBean) redisProvide.get(token);
			Map<String, Object> userInfo = null;
			try {
				userInfo = Object2MapUtil.bean2Map(userBean);
			} catch (IllegalAccessException e) {
				throw new CustomerException(CodeEnum.DATA_PARSE_ERROR);
			}
			redisProvide.del(token);
			token = createToken(userInfo, expired, refresh);
			redisProvide.set(token, userBean);
		}
		return token;
	}
}
